package a5.environment;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import a5.function.Constants;
import a5.function.Function;

/**
 * <pre>
 * This environment emplores a static scoping policy. This environment is best thought of as a stack of "frames" which may be
 * stacked upon one another to create levels of scope. Beginning with the default frame (known as "main"), frames may be 
 * pushed onto the stack (via {@link #pushFrame(String)} ) to create a new level of variables and functions which may be 
 * later removed from the stack (via {@link #popFrame()} ). For example, it is common to push a frame for a function body 
 * definition and pop it at the end of the definition, thus providing the ability to define variables and functions which are
 * not visible from its ancestor frames but are visible to its decendents. 
 * 
 * Scoping policy: Variable and function names must be unique to a frame but may be defined more than once in a scope. That is, 
 * variable and function names may be defined once per Environment instance. To preserve locality, variable lookups start with 
 * the instance on which {@link #getFunction(String)} or {@link #getVariable(String)} is invoked and propogates to 
 * the ancestors if the variable does not exist. It is important to understand that there is no "head" or "tail" to the stack 
 * but rather each Environment instance contains only a reference to its parent. It is in essence, a rudimentary linked list
 * of Environment instances. This allows references to specific Environment instances to be saved and later referenced all the while
 * allowing Environment manipulation to occur. For example, with the following environment stack
 * 
 * env A -> env B -> env C
 * 
 * A function may be defined which references env C. No matter what happens after the function's definition and storage of the reference,
 * this reference will continue to persist. Now suppose the environment continues to build:
 * 
 * env A -> env B -> env C -> env D -> env E
 * 
 * and the function is invoked. Even though env D and env E exist in the stack, the function only has access to env A, env B, and env C 
 * since C's variable and function lookups only propogate to its ancestors and not its children. 
 * 
 * Name conventions: The environment places no requirements on the naming conventions for variables and functions. This has the 
 * side effect of allowing slight inconsistencies with the scoping policies just described since a function may have the same 
 * name as a variable. Therefore, it is a good idea for the grammar defining the language being interpreted to have a naming 
 * convention which discerns variables from functions. 
 * 
 * All setter methods as well as the {@code Maps} which back the variable and function collections
 * are synchonized. Thus, this environment itself is thread safe. 
 * 
 * Note: The {@code a5.environment.function.Function Function} returned by {@link #getFunction(String)}
 * is NOT guaranteed to be thread safe. Thread safety of this class is determined by the 
 * {@code Function} implementation.
 * </pre>
 * 
 * @author Jason Campos
 * @version Assignment 6, CS 152 (Programming Paradigms) Fall 2012, Jeffrey Smith
 */
public class StackedEnvironment implements Environment {
	// Storage for variables 
	protected final Map<String,String> variables;
	// Storage for functions
	protected final Map<String,Function> functions;
	// The previous frame in the stack
	protected StackedEnvironment parent;
	// This frame's name
	protected String name;
	
	/**
	 * Creates a new scope isolation level. Defaults to having no parent environment and the name "main". 
	 */
	public StackedEnvironment(){
		parent = null;
		name = "main";
		variables = Collections.synchronizedMap(new HashMap<String,String>());
		functions = Collections.synchronizedMap(new HashMap<String,Function>());
		functions.putAll(Constants.functions);
		variables.putAll(Constants.variables);
	}
	
	/**
	 * Creates a new scope isolation level and returns a reference to it. Appends this environment's name
	 * to the name of the new environment to create a heirarchy (example: main.f).
	 * @return
	 * A reference to the newly created environment frame.
	 */
	@Override
	public Environment pushFrame(String envName) {
		StackedEnvironment newFrame = new StackedEnvironment();
		newFrame.parent = this;
		newFrame.name = this.name + "." + envName;
		return newFrame;
	}

	/**
	 * Removes a scope isolation level and returns a reference to its parent. Any variables defined in this frame will 
	 * be lost. 
	 * @return
	 * A reference to the parent environment frame.
	 */
	@Override
	public Environment popFrame() {
		return parent;
	}


	/**
	 * (non-Javadoc)
	 * @see a5.environment.Environment#setVariable(java.lang.String)
	 */
	@Override
	public synchronized String setVariable(String variableName, String value){
		if(variables.containsKey(variableName))
			throw new IllegalArgumentException("Duplicate variable name '" + variableName + "' in " + this);
		
		return variables.put(variableName, value);
	}

	/*
	 * (non-Javadoc)
	 * @see a5.environment.Environment#getVariable(java.lang.String)
	 */
	@Override
	public String getVariable(String variableName) throws IllegalArgumentException{
		return getVariable(variableName, this);
	}
	
	/**
	 * Helper method to {@link #getVariable(String)} which tracks the the environment from which the getVariable request
	 * was initiated. This allows the error message to include the location of the request. 
	 * 
	 * @param variableName
	 * The name of the variable to dereference
	 * @param env
	 * The environment from which the getVariable request was initiated.
	 * @return
	 * The value (at the time of invocation) of the variable associated with the environment
	 * @throws IllegalArgumentException
	 * Thrown when no variable exists within the environment by the name {@code variableName}
	 */
	protected String getVariable(String variableName, StackedEnvironment env) throws IllegalArgumentException{
		// Check this frame. If variable doesn't exist, search the parents recursively.
		// If this frame has no parent, throw an illegal argument exception.
		if(variables.containsKey(variableName))
			return variables.get(variableName);
		
		if(parent != null)
			return parent.getVariable(variableName, env);
		else
			throw new IllegalArgumentException("Variable '" + variableName + "' not found in " + env);
	}
	
	/*
	 * (non-Javadoc)
	 * @see a5.environment.Environment#setFunction(java.lang.String, a5.function.Function)
	 */
	@Override
	public synchronized Function setFunction(String functionName, Function function){
		if(functions.containsKey(functionName))
			throw new IllegalArgumentException("Duplicate function name '" + functionName + "' in " + this);
	
		return functions.put(functionName, function);
	}
	
	/*
	 * (non-Javadoc)
	 * @see a5.environment.Environment#getFunction(java.lang.String)
	 */
	@Override
	public Function getFunction(String functionName) throws IllegalArgumentException{
		return getFunction(functionName, this);
	}
	
	/**
	 * Helper method to {@link #getFunction(String)} which tracks the the environment from which the getFunction request
	 * was initiated. This allows the error message to include the location of the request. 
	 * 
	 * @param functionName
	 * The name of the {@code Function} to dereference
	 * @param env
	 * The environment from which the getFunction request was initiated.
	 * @return
	 * The {@code Function} (at the time of invocation) associated with the environment with name {@code functionName}
	 * @throws IllegalArgumentException
	 * Thrown when no {@code Function} exists within the environment with the argument {@code functionName}
	 */
	protected Function getFunction(String functionName, StackedEnvironment env) throws IllegalArgumentException{
		// Check this frame. If function doesn't exist, search the parents recursively.
		// If this frame has no parent, throw an illegal argument exception.
		if(functions.containsKey(functionName))
			return functions.get(functionName);
		
		if(parent != null)
			return parent.getFunction(functionName, env);
		else
			throw new IllegalArgumentException("Function '" + functionName + "' not found in " + env);	}

	/*
	 * (non-Javadoc)
	 * @see a5.environment.Environment#getConstantFunctions()
	 */
	@Override
	public Map<String,Function> getConstantFunctions(){
		return Collections.unmodifiableMap(Constants.functions);
	}

	/*
	 * (non-Javadoc)
	 * @see a5.environment.Environment#getConstantVariables()
	 */
	@Override
	public Map<String, String> getConstantVariables() {
		return Collections.unmodifiableMap(Constants.variables);
	}
	
	@Override
	public String toString(){
		return name;
	}
}
